/* ***** BEGIN LICENSE BLOCK *****
 *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is ThumbsDown.
 *
 * The Initial Developer of the Original Code is
 * Atte Kemppilä.
 * Portions created by the Initial Developer are Copyright (C) 2008
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 * 
 * ***** END LICENSE BLOCK ***** */


const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const Cu = Components.utils;

Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");

Cu.import("resource://thumbsdown/ResourceLoader.jsm");
Cu.import("resource://thumbsdown/ScriptStore.jsm");
Cu.import("resource://thumbsdown/StringArray.jsm");
Cu.import("resource://thumbsdown/ThumbsDown.jsm");
Cu.import("resource://thumbsdown/util.jsm");


const FILTER_MODE_BOTH   = 1;  // both width and height over limit
const FILTER_MODE_EITHER = 2;  // either width or height over limit

const INPUT_DELAY = 100;


var modelConfig =
{
    doc: {},
    referer: {},
    
    // thumbnails
    thumbnails: {                       value: [], equals: arrayEquals},
    selected:   {depends: "thumbnails", value: [], equals: arrayEquals},
    matchingScript: {depends: "selected"},
    thumbnailType:  {depends: "selected"},
    
    // linkType
    linkType:       {depends: "selected"},
    linkExpr:       {depends: "selected linkType"},
    linkUrls:       {depends: "selected linkType linkExpr", value: [], equals: arrayEquals},
    
    // target
    target: {depends: "linkUrls"},
    
    // images (target)
    //imageUrls: {depends: "selected linkUrls", value: [], equals: arrayEquals},
    images: {depends: "selected linkUrls", value: [], equals: arrayEquals},
    
    // mode
    mode:   {depends: "selected"},
    script: {depends: "selected mode", value: null, equals: function(a, b) { return ((a === b) || ((a !== null) && (b !== null) && (a.id === b.id))); }},
    
    // name
    name:        {depends: "linkUrls script"},
    
    // matcher
    thumbnailPattern: {depends: "selected script"},
    linkPattern:      {depends: "linkUrls script"},
    
    // type
    type:        {depends: "linkUrls target script"},
    
    // regexp
    source:      {depends: "images script"},
    pattern:     {depends: "images script"},
    template:    {depends: "images script"},
    
    // wrapped
    attrName:    {depends: "images script"},
    attrValue:   {depends: "images script"},
    
    // error
    error: {}
}

var $ = function(id) { return document.getElementById(id); }
var _;


var scriptWizard =
{
    init: function()
    {
        _ = getStringBundleGetter($("stringbundle"));
        
        var prefButton = wizard.elem.getButton("extra1");
        prefButton.label = _("preferences_");
        prefButton.hidden = false;
        prefButton.addEventListener("command", function() { scriptWizard.openPreferences(); }, false);
        
        wizard.elem.setAttribute("onwizardfinish", "return scriptWizard.onWizardFinish()");
        
        model = new Model(modelConfig);
        
        var doc = window.arguments[0];
        model.doc = doc;
        model.referer = doc.location.href;
        
        var thumbnails = ThumbsDown.getThumbnails(doc);
        thumbnails = unique(thumbnails, function(t1, t2) { return (t1.equals(t2)); });
        thumbnails.forEach(function(t, index) { t.index = index; });
        model.thumbnails = thumbnails;
    },
    
    getMatchingScript: function(thumbnails)
    {
        // look up all the existing scripts that match for each thumbnail
        var matchesArray = [];
        for each (var thumbnail in thumbnails)
        {
            var matches = ThumbsDown.ScriptManager.lookupAll(thumbnail);
            
            // it's not possible to say that wrapped script matches solely based on a thumbnail (attribute must match also)
            // of course, the same is true with custom script also, but we'll ignore that for now
            matches = [script for each (script in matches) if (script.type != "wrapped")]
            
            if (matches.length == 0)
                break;
            
            matchesArray.push(matches);
        }
        
        // is there an existing script that matches all the thumbnails
        if (matchesArray.length == thumbnails.length)
        {
            loop:
            for (var i = 0; i < matchesArray[0].length; i++)
            {
                var script = matchesArray[0][i];
                var isSameScript = function(s) { return (s.id == script.id); };
                for (var k = 1; k < matchesArray.length; k++)
                    if (! matchesArray[k].some(isSameScript))
                        continue loop;
                
                return script;
            }
        }
        
        return null;
    },
    
    getUrlTokens: function(specs)
    {
        var urls = [Services.io.newURI(spec, null, null).QueryInterface(Ci.nsIURL) for each (spec in specs)];
        var urlTokens = new UrlTokens(urls);
        
        if (! urlTokens.schemes.equal)
            throw "scheme.notEqual";
        
        if (! urlTokens.hostTokens.slice(1).every(function(sa) { return sa.equal; }))
            throw "host.notEqual";
        
        if (urlTokens.pathTokens[urlTokens.pathTokens.length - 1].hasNull)
            throw "path.notEqualCount";
        
        return urlTokens;
    },
    
    getEntries: function(arrays)
    {
        let entries = [];
        let names = [name for (name in arrays)];
        let count = arrays[names[0]].length;
        
        // assert that all arrays are the same length
        for each (let name in names)
            if (arrays[name].length != count)
                throw new Error(name + ": " + arrays[name].length + " (" + count + ")");
        
        loop:
        for (let i = 0; i < count; i++)
        {
            let entry = {};
            for each (let name in names)
                if ((entry[name] = arrays[name][i]) === null)
                    continue loop;
            
            // entry.<name>.<name>Url -> entry.<name>Url
            for each (let name in names)
            {
                let nameUrl = name + "Url";
                if ((typeof(entry[name]) == "object") && (nameUrl in entry[name]))
                    entry[nameUrl] = entry[name][nameUrl];
            }
            
            entry.index = entries.length;
            entries.push(entry);
        }
        
        return entries;
    },
    
    getName: function(urlTokens)
    {
        var hostToken = (urlTokens.hostTokens[0].equal ? urlTokens.hostTokens[0].strings[0] : null);
        var index = ((hostToken && (! /^www/.test(hostToken))) ? 0 : 1);
        var name = [sa.strings[0] for each (sa in urlTokens.hostTokens.slice(index))].join(".");
        return name;
    },
    
    getTemplate: function(sourceUrls, targetUrls, pattern)
    {
        if (sourceUrls.length != targetUrls.length)
            throw new Error();
        
        var targets = new StringArray(targetUrls);
        var sources = sourceUrls;
        var re = new RegExp("^" + pattern + "$");
        
        // add common prefix to template and remove it from targets
        var template = targets.prefix;
        targets = targets.substring(targets.prefix.length);
        if (targets.isEmpty)
            return template;
        
        // collect match object for each source
        var matchesArray = [];
        for (var i = 0; i < sources.length; i++)
            matchesArray[i] = re.exec(sources[i]);
        
        // bail out if pattern did not match all the sources
        if (matchesArray.some(function(value) { return (! value); }))
            return template + targets.strings[0];
        
        // collect captures into StringArrays
        var saMatches = [];
        for (var i = 1; i < matchesArray[0].length; i++)
            saMatches.push(new StringArray([match[i] for each (match in matchesArray)]));
        
        // replace "dynamic" parts in targets with captures from sources
        // TODO: captures are used in order. while usually true, not necessarily
        for (var index = 0; index < saMatches.length; index++)
        {
            var matches = saMatches[index];
            if (targets.startsWith(matches))
            {
                template += "$" + (index + 1);
                targets = targets.substr(matches.lengths());
                template += targets.prefix;
                targets = targets.substr(targets.prefix.length);
            }
        }
        
        if (! targets.isEmpty)
            return template + targets.strings[0];
        
        return template;
    },
    
    resolveUrlRegExp: function(urlTokens, capture)
    {
        // TODO: eh, ever heard about non-greedy quantifiers? replace sentinels?
        var sentinels;
        var tokens;
        
        var hasQueries = (! urlTokens.queries.isEmpty);
        var hasRefs = (! urlTokens.refs.isEmpty);
        
        // scheme
        var scheme = this.resolveRegExp(urlTokens.schemes);
        
        // host
        sentinels = arrayWithValue(urlTokens.hostTokens.length, ".");
        sentinels[sentinels.length - 1] = "/";
        tokens = [];
        for (var i = 0; i < urlTokens.hostTokens.length; i++)
            tokens.push(this.resolveRegExp(urlTokens.hostTokens[i], capture, sentinels[i]));
        var host = tokens.join(escapeRegExp("."));
        
        // path
        sentinels = arrayWithValue(urlTokens.pathTokens.length, "/");
        sentinels[sentinels.length - 1] = (hasQueries ? "?" : (hasRefs ? "#" : null))
        tokens = [];
        for (var i = 0; i < urlTokens.pathTokens.length; i++)
            tokens.push(this.resolveRegExp(urlTokens.pathTokens[i], capture, sentinels[i]));
        var path = tokens.join(escapeRegExp("/"));
        
        // query
        sentinels = arrayWithValue(urlTokens.queryTokens.length, "&");
        sentinels[sentinels.length - 1] = (hasRefs ? "#" : null);
        tokens = [];
        for (var i = 0; i < urlTokens.queryTokens.length; i++)
            tokens.push(this.resolveRegExp(urlTokens.queryTokens[i], capture, sentinels[i]));
        var query = tokens.join(escapeRegExp("&"));
        if (query)
            query = escapeRegExp("?") + query;
        
        // reference
        var reference = this.resolveRegExp(urlTokens.refs);
        if (reference)
            reference = escapeRegExp("#") + reference;
        
        return scheme + "://" + host + path + query + reference;
    },
    
    resolveRegExp: function(sa, capture, sentinel)
    {
        if (sa.equal)
            return escapeRegExp(sa.strings[0]);
        
        var prefix = escapeRegExp(sa.prefix);
        var middle = this._resolveRegExp(sa.middle(), sentinel);
        var suffix = escapeRegExp(sa.suffix);
        
        if (capture)
            middle = "(" + middle +")";
        
        return prefix + middle + suffix;
    },
    
    _resolveRegExp: function(sa, sentinel)
    {
        var p = "";
        var n = sa.size;
        var metaCharClass = null;
        var charClass = "";
        var pattern = null;
        var quantifier = ((sa.length.min == 0) ? "*" : "+");
        
        var i = 0, k = 0;
        while ((i < n) && (sa.strings[i].length == 0))
            i++;
        
        while (i < n)
        {
            var str = sa.strings[i];
            var c = str.charAt(k);
            
            if ((! metaCharClass) && /\d/.test(c))
                metaCharClass = "\\d";
            else if ((metaCharClass != "\\w") && /\w/.test(c))
                metaCharClass = "\\w";
            else if (charClass.length < 1)
                charClass = ((c == "-") ? c + charClass : charClass + c);
            else if (! pattern)
                pattern = (sentinel ? "[^" + sentinel + "]" : ".");
            else
                throw new Error("unreachable code");
            
            var p = (pattern ? pattern : (charClass ? "[" + escapeRegExp(charClass) + metaCharClass + "]" : metaCharClass)) + quantifier;
            var re = new RegExp("^" + p);
            
            for (i = 0; i < n; i++)
            {
                str = sa.strings[i];
                var match = re.exec(str);
                if (! match)
                {
                    k = 0;
                    break;
                }
                if (match[0].length != str.length)
                {
                    k = match[0].length;
                    break;
                }
            }
        }
        
        return p;
    },
    
    openPreferences: function()
    {
        let instantApply = Services.prefs.getBoolPref("browser.preferences.instantApply");
        
        openWindow({
             parent: window,
             url: "chrome://thumbsdown/content/scriptwizard/prefwindow.xul",
             name: "thumbsdown_scriptwizard_prefwindow",
             features: "chrome,titlebar,toolbar,centerscreen" + (instantApply ? ",dialog=no" : ",modal"),
        });
    },
    
    onWizardFinish: function()
    {
        if (model.matchingScript)
            return true;
        
        var script = {};
        
        if (model.mode == "modify")
            script.id = model.script.id;
        
        script.name = model.name;
        script.disabled = false;
        script.type = model.type;
        script.subType = null;
        script.thumbnailType = model.thumbnailType;
        script.thumbnailPattern = model.thumbnailPattern;
        script.linkType = model.linkType;
        script.linkExpr = model.linkExpr;
        script.linkPattern = model.linkPattern;
        
        switch (model.type)
        {
            case "regexp":
                script.subType = model.source;
                script.action1 = model.pattern;
                script.action2 = model.template;
                break;
                
            case "wrapped":
                script.action1 = model.attrName;
                script.action2 = model.attrValue;
                break;
        }
        
        if (model.mode == "modify")
            ScriptStore.setScript(script);
        else
            ScriptStore.addScript(script);
        
        return true;
    }
}

wizard.addLoadListener(function() { scriptWizard.init(); });


function UrlTokens(urls)
{
    this.urls = urls;
    
    this.schemes = new StringArray([url.scheme   for each (url in urls)]);
    this.hosts   = new StringArray([url.host     for each (url in urls)]);
    this.paths   = new StringArray([url.filePath for each (url in urls)]);
    this.queries = new StringArray([url.query    for each (url in urls)]);
    this.refs    = new StringArray([url.ref      for each (url in urls)]);
    
    this.hostTokens = this.hosts.tokenize(".");
    this.pathTokens = this.paths.tokenize("/");
    this.queryTokens = this.queries.tokenize("&");
}


function HeadLoader(link, referer, timeout, callback)
{
    this.callback = callback;
    
    // TODO: use nsiXHR? XHR "stops" when wizard page is changed
    this.request = new XMLHttpRequest();
    this.request.open("HEAD", link, true);
    this.request.setRequestHeader("Referer", referer);
    this.request.timeout = timeout;
    var self = this;
    this.request.onload = function() { self.wrap(self.onLoad); };
    this.request.onerror = function() { self.wrap(self.onError); };
    this.request.ontimeout = function() { self.wrap(self.onTimeout); };
    this.request.send(null);
}

HeadLoader.prototype =
{
    wrap: function(func)
    {
        try
        {
            if (this.success === undefined)
                func.apply(this);
            if (this.callback)
                this.callback(this);
        }
        finally
        {
            this.destroy();
        }
    },
    
    onLoad: function()
    {
        this.success = true;
    },
    
    onError: function()
    {
        this.success = false;
    },
    
    onTimeout: function()
    {
        this.success = false;
        this.timeout = true;
    },
    
    destroy: function()
    {
        if (this.request)
        {
            this.request.abort();
            delete this.request;
        }
        
        delete this.callback;
    }
}
